"""Build level coverage manager"""

# pylint: disable=C0301:line-too-long

import json
from os import walk, listdir
from os.path import exists, join
from typing import List
import webbrowser

from loguru import logger

from ios_cov_manager.config.path import (
    BUILD_DIST_DIR,
    OUTPUT_DIR,
    PROFRAW_DIR,
    REPOS_DIR,
)
from ios_cov_manager.lib.profraw_util import ProfrawUtil
from ios_cov_manager.model.branch_coverage import (
    BranchCoverage,
)
from ios_cov_manager.lib.cmd_util import execute_command
from ios_cov_manager.lib.pod_util import pod_update
from ios_cov_manager.lib.string_util import (
    extract_paths_not_found,
    parse_device_profraw_file_name,
)
from ios_cov_manager.lib.system_util import (
    find_file_in_directory,
    make_dir_if_not_exists,
)
from ios_cov_manager.lib.disk_cache_util import DiskCacheUtil


class VersionBuildCoverageManager(BranchCoverage):
    """Version and build level coverage manager"""

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)

    def get_build_level_coverage_output_dir(self, build_number: str):
        """Get the build level coverage output directory"""

        return join(
            OUTPUT_DIR,
            self.version,
            self.replaced_branch_name,
            build_number,
            "results",
        )

    def get_version_level_coverage_output_dir(self) -> str:
        """Get the version level coverage output directory path"""

        return join(
            OUTPUT_DIR,
            self.version,
            self.replaced_branch_name,
            "results",
        )

    def get_version_level_profraw_dir(self) -> str:
        """Get the version level profraw directory path"""

        return join(PROFRAW_DIR, self.version, self.replaced_branch_name)

    def get_build_level_dist_dir(self, build_number: str):
        """Get the build level dist directory"""

        return join(
            BUILD_DIST_DIR,
            self.version,
            self.replaced_branch_name,
            build_number,
        )

    def get_build_level_macho_files_dir(self, build_number: str):
        """Get the build level macho files directory"""

        return join(
            self.get_build_level_dist_dir(build_number=build_number),
            "macho_files",
        )

    def get_build_level_debug_iphoneos_dir(self, build_number: str):
        """Get the build level debug-iphoneos directory"""

        return join(
            self.get_build_level_dist_dir(build_number=build_number),
            "Debug-iphoneos",
        )

    def get_build_level_profraw_dir(self, build_number: str):
        """Get the build level profraw directory"""

        return join(
            PROFRAW_DIR,
            self.version,
            self.replaced_branch_name,
            build_number,
        )

    def get_build_level_ios_repo_parent_dir(self, build_number: str):
        """Get the build level iOS repo parent directory"""

        return join(
            REPOS_DIR,
            self.version,
            self.replaced_branch_name,
            build_number,
        )

    def get_build_level_ios_repo_dir(self, build_number: str):
        """Get the build level iOS repo directory"""

        return (
            join(
                self.get_build_level_ios_repo_parent_dir(build_number=build_number),
                self.ios_repo_name,
            ).rstrip("/")
            + "/"
        )

    def get_build_level_macho_dir_path(self, build_number: str):
        """Get the path to a directory containing mach-o files for a given build"""

        return join(
            BUILD_DIST_DIR,
            self.version,
            self.replaced_branch_name,
            build_number,
            "macho_files",
        )

    def get_build_level_coverage_profdata_txt_path(
        self, build_number: str, macho_file_name: str
    ):
        """Get the build level profdata txt path"""

        build_level_coverage_output_dir = self.get_build_level_coverage_output_dir(
            build_number=build_number
        )
        return join(build_level_coverage_output_dir, f"{macho_file_name}_profdata.txt")

    def get_version_level_coverage_profdata_txt_path(self, macho_file_name: str):
        """Get the version level profdata txt path"""

        version_level_coverage_output_dir = self.get_version_level_coverage_output_dir()
        return join(
            version_level_coverage_output_dir, f"{macho_file_name}_profdata.txt"
        )

    def get_build_level_coverage_results_main_dir(
        self, build_number: str, macho_file_name: str
    ):
        """Get the build level coverage results main directory"""

        return join(
            self.get_build_level_coverage_output_dir(build_number=build_number),
            macho_file_name,
        )

    def get_build_level_coverage_index_html_path(
        self, build_number: str, macho_file_name: str
    ):
        """Get the build level coverage index html path"""

        return join(
            self.get_build_level_coverage_results_main_dir(
                build_number=build_number, macho_file_name=macho_file_name
            ),
            "index.html",
        )

    def get_build_level_coverage_key(self, build_number: str):
        """Get the key used to store the build level coverage data"""

        return f"{self.version}_{self.replaced_branch_name}_{build_number}"

    def get_version_level_coverage_key(self):
        """Get the key used to store the version level coverage data"""

        return f"{self.version}_{self.replaced_branch_name}"

    def get_the_latest_build_number(self) -> str:
        """Get the latest build number under the version"""

        return max(self.collect_build_numbers())

    def get_coverage_data(self, profdata_txt_path: str):
        """Get the coverage data from the index.html file"""

        with open(profdata_txt_path, encoding="utf-8") as f:
            content = json.load(f)
            data = content["data"][0]
            totals = data["totals"]
            lines = totals["lines"]
            return lines

    def get_build_level_coverage_data(self, build_number: str, macho_file_name: str):
        """Get the coverage data from the index.html file"""

        build_level_coverage_profdata_txt: str = (
            self.get_build_level_coverage_profdata_txt_path(
                build_number=build_number, macho_file_name=macho_file_name
            )
        )
        return self.get_coverage_data(
            profdata_txt_path=build_level_coverage_profdata_txt
        )

    def get_version_level_coverage_data(self, macho_file_name: str):
        """Get the coverage data from the index.html file"""

        version_level_coverage_profdata_txt: str = (
            self.get_version_level_coverage_profdata_txt_path(
                macho_file_name=macho_file_name
            )
        )
        return self.get_coverage_data(
            profdata_txt_path=version_level_coverage_profdata_txt
        )

    def find_macho_file(self, build_number: str, macho_file_name: str):
        """Find the path to the macho file in the macho_files directory"""

        build_level_macho_files_dir = self.get_build_level_macho_files_dir(
            build_number=build_number
        )
        macho_file_path = find_file_in_directory(
            build_level_macho_files_dir,
            macho_file_name
            if macho_file_name.endswith(".o")
            else f"{macho_file_name}.o",
        )
        if not macho_file_path:
            logger.error(
                f"Macho {macho_file_name} not found under: {build_level_macho_files_dir}"
            )
            raise FileNotFoundError
        return macho_file_path

    def collect_build_numbers(self) -> List[str]:
        """Collect build numbers information"""

        if not exists(PROFRAW_DIR):
            raise FileNotFoundError(f"Profraw directory doesn't exist: {PROFRAW_DIR}")

        version_branch_dir_name = join(
            PROFRAW_DIR, self.version, self.replaced_branch_name
        )
        build_numbers = []
        for _, dirs, _ in walk(version_branch_dir_name):
            for _dir in dirs:
                build_numbers.append(_dir)
        return build_numbers

    def deploy_build_level_framework_dirs(self, build_number: str):
        """Deploy the build level framework directories"""

        self.deploy_build_level_coverage_output_dir(build_number=build_number)
        self.deploy_build_level_dist_dir(build_number=build_number)
        self.deploy_build_level_macho_files_dir(build_number=build_number)
        self.deploy_build_level_debug_iphoneos_dir(build_number=build_number)
        self.deploy_build_level_repo_parent_dir(build_number=build_number)
        self.deploy_build_level_ios_repo_dir(build_number=build_number)
        logger.success(
            f"Build level framework directories have been deployed, build number is {build_number}"
        )

    def deploy_version_level_framework_dirs(self):
        """Deploy the version level framework directories"""

        self.deploy_version_level_coverage_output_dir()
        self.deploy_version_level_dist_dir()
        self.deploy_version_level_macho_files_dir()
        self.deploy_version_level_debug_iphoneos_dir()
        self.deploy_version_level_repo_parent_dir()
        self.deploy_version_level_ios_repo_dir()
        logger.success(
            f"Version level framework directories have been deployed, version is {self.version}"
        )

    def deploy_build_level_coverage_output_dir(self, build_number: str):
        """Deploy the build level coverage output directory"""

        make_dir_if_not_exists(
            self.get_build_level_coverage_output_dir(build_number=build_number)
        )

    def deploy_version_level_coverage_output_dir(self):
        """Deploy the version level coverage output directory"""

        version_level_coverage_output_dir = self.get_version_level_coverage_output_dir()
        make_dir_if_not_exists(version_level_coverage_output_dir)

    def deploy_build_level_dist_dir(self, build_number: str):
        """Deploy the build level dist directory"""

        make_dir_if_not_exists(
            path=self.get_build_level_dist_dir(build_number=build_number)
        )

    def deploy_version_level_dist_dir(self):
        """Deploy the version level dist directory"""

        build_numbers = self.collect_build_numbers()
        for build_number in build_numbers:
            make_dir_if_not_exists(
                self.get_build_level_dist_dir(build_number=build_number)
            )

    def deploy_build_level_debug_iphoneos_dir(self, build_number: str):
        """Deploy the build level debug-iphoneos directory"""

        make_dir_if_not_exists(
            self.get_build_level_debug_iphoneos_dir(build_number=build_number)
        )

    def deploy_version_level_debug_iphoneos_dir(self):
        """Deploy the version level debug-iphoneos directory"""

        build_numbers = self.collect_build_numbers()
        for build_number in build_numbers:
            make_dir_if_not_exists(
                self.get_build_level_debug_iphoneos_dir(build_number=build_number)
            )

    def deploy_build_level_macho_files_dir(self, build_number: str):
        """Deploy the build level macho_files directory"""

        make_dir_if_not_exists(
            self.get_build_level_macho_files_dir(build_number=build_number)
        )

    def deploy_version_level_macho_files_dir(self):
        """Deploy the version level macho_files directory"""

        build_numbers = self.collect_build_numbers()
        for build_number in build_numbers:
            make_dir_if_not_exists(
                self.get_build_level_macho_files_dir(build_number=build_number)
            )

    def deploy_build_level_repo_parent_dir(self, build_number: str):
        """Deploy the build level repo parent directory"""

        make_dir_if_not_exists(
            self.get_build_level_ios_repo_parent_dir(build_number=build_number)
        )

    def deploy_version_level_repo_parent_dir(self):
        """Deploy the version level repo parent directory"""

        build_numbers = self.collect_build_numbers()
        for build_number in build_numbers:
            make_dir_if_not_exists(
                self.get_build_level_ios_repo_parent_dir(build_number=build_number)
            )

    def deploy_build_level_ios_repo_dir(self, build_number: str):
        """Deploy the build level iOS repo directory"""

        build_level_ios_repo_dir = self.get_build_level_ios_repo_dir(
            build_number=build_number
        )
        build_level_ios_repo_parent_dir = self.get_build_level_ios_repo_parent_dir(
            build_number=build_number
        )

        # Clone the iOS repository
        if exists(build_level_ios_repo_dir):
            logger.info(
                f"Repository {build_level_ios_repo_dir} has been cloned, skip cloning it."
            )
        else:
            execute_command(
                command=f"git clone {self.ios_repo_git_url}",
                cwd=build_level_ios_repo_parent_dir,
            )
        if not exists(build_level_ios_repo_dir):
            raise FileNotFoundError(
                f"The iOS repository hasn't been cloned yet: {build_level_ios_repo_dir}"
            )

        # Checkout the specified branch, pull and checkout to the specific commit
        execute_command(command="git reset --hard", cwd=build_level_ios_repo_dir)
        execute_command(
            command=f"git checkout {self.branch_name}",
            cwd=build_level_ios_repo_dir,
        )
        execute_command(command="git pull", cwd=build_level_ios_repo_dir)
        execute_command(
            command=f"git checkout {self.commit_id}", cwd=build_level_ios_repo_dir
        )

    def deploy_version_level_ios_repo_dir(self):
        """Deploy the version level iOS repo directory"""

        build_numbers = self.collect_build_numbers()
        for build_number in build_numbers:
            self.deploy_build_level_ios_repo_dir(build_number=build_number)

    def update_build_level_ios_repo_pods(self, build_number: str) -> None:
        """Update pods"""

        pod_update(
            ios_repo_dir=self.get_build_level_ios_repo_dir(build_number=build_number)
        )

    def collect_build_level_macho_file_names(self, build_number: str):
        """Collect all the names of the macho files"""

        build_level_macho_files_dir = self.get_build_level_macho_files_dir(
            build_number=build_number
        )
        return [
            name for name in listdir(build_level_macho_files_dir) if name.endswith(".o")
        ]

    def show_build_level_index_html(self, build_number: str, macho_file_name: str):
        """Show the index.html file using the default browser"""

        build_level_coverage_index_html_path: str = (
            self.get_build_level_coverage_index_html_path(
                build_number=build_number, macho_file_name=macho_file_name
            )
        )
        webbrowser.open(f"file://{build_level_coverage_index_html_path}")

    def merge_build_level_profraws_into_html(self, build_number: str) -> dict:
        """Merge the profraw files with the same build and generate the index.html file

        return: All the files that can not be found
        """

        build_level_macho_files_dir = self.get_build_level_macho_files_dir(
            build_number=build_number
        )
        build_level_profraw_dir = self.get_build_level_profraw_dir(
            build_number=build_number
        )
        build_level_coverage_output_dir = self.get_build_level_coverage_output_dir(
            build_number=build_number
        )
        build_level_ios_repo_dir = self.get_build_level_ios_repo_dir(
            build_number=build_number
        )

        macho_profraws_map = {}

        # Get all the profraw files under the directory and the macho file name
        for _, _, files in walk(build_level_profraw_dir):
            for file in files:
                if ".profraw" in file:
                    macho_file_name = parse_device_profraw_file_name(file)[1]
                    if macho_file_name not in macho_profraws_map:
                        macho_profraws_map[macho_file_name] = []
                    else:
                        macho_profraws_map[macho_file_name].append(file)

        logger.debug(f"The macho profraws map is: {macho_profraws_map}")

        # Merge profraw files for each macho file
        macho_error_lines = {}
        for macho_file_name, profraw_names in macho_profraws_map.items():
            paths = " ".join(profraw_names)

            macho_file_path = join(build_level_macho_files_dir, f"{macho_file_name}.o")
            _error_lines = ProfrawUtil.merge_profraws_into_html(
                paths=paths,
                output_dir_path=build_level_coverage_output_dir,
                macho_file_path=macho_file_path,
                build_machine_repo_path=self.build_machine_repo_path,
                coverage_machine_repo_path=build_level_ios_repo_dir,
            )
            macho_error_lines[macho_file_name] = _error_lines

        macho_not_found_files = {}
        for macho_file_name, error_lines in macho_error_lines.items():
            paths_not_found = extract_paths_not_found(error_lines=error_lines)
            macho_not_found_files[macho_file_name] = paths_not_found

        return macho_not_found_files

    def merge_version_level_profraws_into_html(self) -> dict:
        """Merge the profraw files with the same version and generate the index.html file

        The macho files and the ios repository of the latest build number will be used to generate the version level index.html file

        return: All the files that can not be found
        """

        latest_build_number = self.get_the_latest_build_number()
        latest_build_ios_repo_dir = self.get_build_level_ios_repo_dir(
            build_number=latest_build_number
        )
        latest_build_macho_files_dir = self.get_build_level_macho_files_dir(
            build_number=latest_build_number
        )
        version_level_coverage_output_dir = self.get_version_level_coverage_output_dir()
        version_level_profraw_dir_path = self.get_version_level_profraw_dir()

        macho_error_lines = {}
        # Iterate over all the build level directories
        for root, dirs, _ in walk(version_level_profraw_dir_path):
            for dir_name in dirs:
                # Merge the profraw files into one html report file
                macho_file_dir_path = self.get_build_level_macho_dir_path(
                    build_number=dir_name
                )

                macho_profraws_map = {}

                # Get all the profraw files under the directory and the macho file name
                for _, _, files in walk(version_level_profraw_dir_path):
                    for file in files:
                        if ".profraw" in file:
                            macho_file_name = parse_device_profraw_file_name(file)[1]
                            if macho_file_name not in macho_profraws_map:
                                macho_profraws_map[macho_file_name] = []
                            else:
                                macho_profraws_map[macho_file_name].append(file)

                # Merge profraw files for each macho file
                macho_error_lines = {}
                for macho_file_name, profraw_names in macho_profraws_map.items():
                    paths = " ".join(profraw_names)
                    macho_file_path = join(macho_file_dir_path, f"{macho_file_name}.o")

                    # Merge all the profraw files into one profdata file
                    # The paths may be very long, so we need to use the `-cwd` option in order to avoid `the command is

        macho_not_found_files = {}
        for build_number, macho_error_lines in builds_macho_error_lines.items():
            for macho_file_name, error_lines in macho_error_lines.items():
                paths_not_found = extract_paths_not_found(error_lines=error_lines)
                if build_number not in macho_not_found_files:
                    macho_not_found_files[build_number] = {}
                macho_not_found_files[build_number][macho_file_name] = paths_not_found
        return macho_not_found_files

    def calc_build_level_coverage(self, build_number) -> float:
        """Calculate the build level coverage data"""

        self.deploy_build_level_framework_dirs(build_number=build_number)

        build_level_coverage_key = self.get_build_level_coverage_key(
            build_number=build_number
        )

        if DiskCacheUtil.has(key=build_level_coverage_key):
            # No need to update the pods if the coverage data had already been calculated before
            logger.debug(f"No need to update the pods for build: {build_number}")
        else:
            logger.debug(f"Need to update the pods for build: {build_number}")
            self.update_build_level_ios_repo_pods(build_number=build_number)

        build_level_macho_error_lines = self.merge_build_level_profraws_into_html(
            build_number=build_number
        )

        coverage_data = {}
        for macho_file_name, error_lines in build_level_macho_error_lines.items():
            if error_lines:
                logger.error(f"{macho_file_name}: {error_lines}")
                continue
            coverage_data[macho_file_name] = self.get_build_level_coverage_data(
                build_number=build_number, macho_file_name=macho_file_name
            )
            # Record the build level coverage data
            DiskCacheUtil.set(key=build_level_coverage_key, value=coverage_data)

        return coverage_data

    def calc_version_level_coverage(self) -> float:
        """Calculate the version level coverage data"""

        self.deploy_version_level_framework_dirs()

        latest_build_number = self.get_the_latest_build_number()

        latest_build_coverage_key = self.get_build_level_coverage_key(
            build_number=latest_build_number
        )

        if DiskCacheUtil.has(key=latest_build_coverage_key):
            # No need to update the pods if the coverage data had already been calculated before
            logger.debug("No need to update the pods")
        else:
            logger.debug("Need to update the pods")
            self.update_build_level_ios_repo_pods(build_number=latest_build_number)

        version_level_macho_error_lines = self.merge_version_level_profraws_into_html()
        logger.debug(
            f"version_level_macho_error_lines: {version_level_macho_error_lines}"
        )

        coverage_data = {}
        for build_number, macho_error_lines in version_level_macho_error_lines.items():
            for macho_file_name, error_lines in macho_error_lines.items():
                if error_lines:
                    logger.error(
                        f"Build {build_number} {macho_file_name}: {error_lines}"
                    )
                    continue
                if macho_file_name not in coverage_data:
                    logger.info(
                        f"Calculate the coverage data for {macho_file_name} under build {build_number}"
                    )
                    coverage_data[macho_file_name] = (
                        self.get_version_level_coverage_data(
                            macho_file_name=macho_file_name
                        )
                    )

        return coverage_data
